import modem
import ujson
from umqtt import MQTTClient as BaseMQTTConnection
from usr.qframe import CurrentApp
from usr.qframe.logging import getLogger
from usr.qframe.threading import Lock, Condition, Thread
from usr.extensions.module_ota import ModuleUpgradeApi


logger = getLogger(__name__)

# 产品id
PID = "4G_tracker"
TOPIC_PREFIX_FORMAT = "$AMG/{pid}/{IMEI}/".format(pid=PID, IMEI=modem.getDevImei())

# 设备到平台
# 1.设备数据上传（数据上传）
DATA = TOPIC_PREFIX_FORMAT + "data"
DATA_REPLY = TOPIC_PREFIX_FORMAT + "data_reply"
# 2.批量GPS数据上报
PACK = TOPIC_PREFIX_FORMAT + "pack"
PACK_REPLY = TOPIC_PREFIX_FORMAT + "pack_reply"
# 3.事件（报警信息）
EVENT = TOPIC_PREFIX_FORMAT + "event"
EVENT_REPLY = TOPIC_PREFIX_FORMAT + "event_reply"

# 平台到设备
# 1.设置设备属性（菜单）
PROPERTY_SET = TOPIC_PREFIX_FORMAT + "property/set"
PROPERTY_SET_REPLY = TOPIC_PREFIX_FORMAT + "property/set_reply"
PROPERTY_GET = TOPIC_PREFIX_FORMAT + "property/get"
PROPERTY_GET_REPLY = TOPIC_PREFIX_FORMAT + "property/get_reply"
# 2.设备命令下发 （传感器，GPS）
CMD = TOPIC_PREFIX_FORMAT + "cmd/request"
CMD_REPLY = TOPIC_PREFIX_FORMAT + "cmd/response"
# 3.服务
SERVICE_INVOKE = TOPIC_PREFIX_FORMAT + "service/invoke"
SERVICE_INVOKE_REPLY = TOPIC_PREFIX_FORMAT + "service/invoke_reply"
# 4.OTA远程升级通信主题
OTA_INFORM = TOPIC_PREFIX_FORMAT + "ota/inform"
OTA_INFORM_REPLY = TOPIC_PREFIX_FORMAT + "ota/inform_reply"


class MQTTConnection(object):

    def __init__(self, *args, **kwargs):
        """init umqtt.MQTTClient instance.
        args:
            client_id - 客户端 ID，字符串类型，具有唯一性。
            server - 服务端地址，字符串类型，可以是 IP 或者域名。
        kwargs:
            port - 服务器端口（可选），整数类型，默认为1883，请注意，MQTT over SSL/TLS的默认端口是8883。
            user - （可选) 在服务器上注册的用户名，字符串类型。
            password - （可选) 在服务器上注册的密码，字符串类型。
            keepalive - （可选）客户端的keepalive超时值，整数类型，默认为0。
            ssl - （可选）是否使能 SSL/TLS 支持，布尔值类型。
            ssl_params - （可选）SSL/TLS 参数，字符串类型。
            reconn - （可选）控制是否使用内部重连的标志，布尔值类型，默认开启为True。
            version - （可选）选择使用mqtt版本，整数类型，version=3开启MQTTv3.1，默认version=4开启MQTTv3.1.1。
            clean_session - 布尔值类型，可选参数，一个决定客户端类型的布尔值。 如果为True，那么代理将在其断开连接时删除有关此客户端的所有信息。
                如果为False，则客户端是持久客户端，当客户端断开连接时，订阅信息和排队消息将被保留。默认为True。
            qos - MQTT消息服务质量（默认0，可选择0或1）.
                整数类型 0：发送者只发送一次消息，不进行重试 1：发送者最少发送一次消息，确保消息到达Broker。
        """
        self.args = args
        self.kwargs = kwargs
        self.clean_session = kwargs.pop('clean_session', True)
        self.__cli = None

    @property
    def cli(self):
        if self.__cli is None:
            raise ValueError('Mqtt Unbound error')
        return self.__cli

    def connect(self):
        self.__cli = BaseMQTTConnection(*self.args, **self.kwargs)
        self.__cli.set_callback(self.recv_callback)
        self.__cli.connect(clean_session=self.clean_session)

    def disconnect(self):
        if self.__cli is not None:
            self.__cli.disconnect()
            self.__cli = None

    def is_status_ok(self):
        return self.__cli is not None and self.__cli.get_mqttsta() == 0

    def publish(self, topic, msg, retain=False, qos=0):
        return self.cli.publish(topic, msg, retain=retain, qos=qos)

    def subscribe(self, topic, qos=0):
        self.cli.subscribe(topic, qos=qos)

    def recv_callback(self, topic, data):
        raise NotImplementedError('this method must be implemented!!!')


class BaseMQTTClient(MQTTConnection):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.__recv_thread = None

    def __recv_thread_worker(self):
        while True:
            try:
                self.cli.wait_msg()
            except Exception as e:
                logger.error('mqtt listen error: {}'.format(str(e)))
                break

    def __start_recv_thread(self):
        self.__recv_thread = Thread(target=self.__recv_thread_worker)
        self.__recv_thread.start()

    def __stop_recv_thread(self):
        if self.__recv_thread:
            self.__recv_thread.terminate()
            self.__recv_thread = None

    def disconnect(self):
        try:
            self.__stop_recv_thread()
            super().disconnect()
        except Exception as e:
            logger.error('mqtt disconnect failed: {}'.format(e))
            return False
        return True

    def connect(self):
        logger.info('mqtt connecting...')
        try:
            super().connect()
        except Exception as e:
            logger.error('mqtt connect failed. {}'.format(str(e)))
            return False
        logger.info('mqtt connect successfully.')
        self.__start_recv_thread()
        return True


class RespHelper(Condition):

    def __init__(self):
        super().__init__()
        self.__resp_map = {}

    def set(self, topic, data):
        self.__resp_map[topic] = data
        self.notify_all()

    def get(self, topic, timeout=10):
        self.__resp_map[topic] = None
        self.wait_for(lambda: self.__resp_map[topic], timeout=timeout)
        return self.__resp_map.pop(topic)


class MqttClientV2(BaseMQTTClient):

    def __init__(self, *args, **kwargs):
        self.resp_helper = RespHelper()
        super().__init__(*args, **kwargs)

    def recv_callback(self, topic, data):
        logger.debug('{} get topic: {}; data: {}'.format(type(self).__name__, topic, data))
        topic = topic.decode()
        data = data.decode()
        if topic in (
            DATA_REPLY,
            PACK_REPLY,
            EVENT_REPLY
        ):
            with self.resp_helper:
                self.resp_helper.set(topic, ujson.loads(data))
        else:
            Thread(target=self.__topic_handler, args=(topic, ujson.loads(data))).start()

    def __topic_handler(self, topic, data):
        logger.debug("{} __topic_handler topic: {}, data: {}".format(type(self).__name__, topic, data))
        try:
            if topic == PROPERTY_SET:
                self.handle_property_set(data)
            elif topic == PROPERTY_GET:
                self.handle_property_get(data)
            elif topic == CMD:
                self.handle_cmd(data)
            elif topic == SERVICE_INVOKE:
                self.handle_service_invoke(data)
            elif topic == OTA_INFORM:
                self.handle_ota_inform(data)
            else:
                logger.error('{} can not handler unknown topic')
        except Exception as e:
            logger.error('__topic_handler got exception: {}'.format(e))

    # -------cloud to device below--------

    def handle_property_set(self, data):
        logger.debug('{} handle_property_set'.format(type(self).__name__))

    def handle_property_get(self, data):
        logger.debug('{} handle_property_get'.format(type(self).__name__))

    def handle_cmd(self, data):
        logger.debug('{} handle_cmd'.format(type(self).__name__))

    def handle_service_invoke(self, data):
        logger.debug('{} handle_service_invoke'.format(type(self).__name__))

    def handle_ota_inform(self, data):
        logger.debug('{} handle_ota_inform'.format(type(self).__name__))
        ota_info = data['ota']
        version = ota_info['version']
        url = ota_info['url']
        ModuleUpgradeApi.upgrade(version, url)

    # -------device to cloud below--------

    def publish_data(self, data):
        with self.resp_helper:
            self.publish(DATA, ujson.dumps(data))
            return self.resp_helper.get(DATA_REPLY)

    def publish_pack_data(self, data):
        data_to_send = {
            "msg_id": 10000001,
            "version": "1.0",
            "number": 1,
            "data": [
                {
                    "msg_id": 10000001,
                    "version": "1.0",
                    "sensors": {
                        "temperatrue": 30,
                        "humidity": 30,
                        "power": 4.0,
                        "strength": 99,
                        "net_type": 1,
                        "cds": 90
                    },
                    "gps": {
                        "longitude": 114.096909,
                        "latitude": 22.555075,
                        "speed": 12.54,
                        "alititude": 99,
                        "degrees": 90,
                        "date": 20240722,
                        "time": "00:38:12"
                    },
                    "connection": {
                        "state": "online",
                        "timestamp": 20240722003812001,
                    }
                }
            ]
        }

        with self.resp_helper:
            self.publish(PACK, ujson.dumps(data_to_send))
            return self.resp_helper.get(PACK_REPLY)

    def publish_event(self, data):
        with self.resp_helper:
            self.publish(EVENT, ujson.dumps(data))
            return self.resp_helper.get(EVENT_REPLY)


class MqttClientAgent(object):

    def __init__(self, app=None):
        self.__client_ref = None
        self.lock = Lock()
        if app:
            self.init_app(app)

    def init_app(self, app):
        app.register('mqtt_client_agent', self)

    def load(self):
        logger.debug('load {}'.format(type(self).__name__))
        return self.client

    def __subscribe_topics(self):
        for topic in (
            DATA_REPLY,
            PACK_REPLY,
            EVENT_REPLY,
            PROPERTY_SET,
            PROPERTY_GET,
            CMD,
            SERVICE_INVOKE,
            OTA_INFORM,
        ):
            logger.debug('subscribe topic: {}'.format(topic))
            self.__client_ref.subscribe(topic)

    def connect(self):
        mqtt_config = CurrentApp().config["MQTT"]
        mqtt_config['client_id'] = modem.getDevImei()
        self.__client_ref = MqttClientV2(**mqtt_config, reconn=False)
        if not self.__client_ref.connect():
            return False
        try:
            self.__subscribe_topics()
        except Exception as e:
            logger.error('subscribe failed: {}'.format(e))
            self.__client_ref.disconnect()
            return False
        else:
            return True

    def disconnect(self):
        if self.__client_ref:
            self.__client_ref.disconnect()
            self.__client_ref = None

    @property
    def client(self):
        with self.lock:
            if self.__client_ref and self.__client_ref.is_status_ok():
                return self.__client_ref
            else:
                self.disconnect()
                if not self.connect():
                    raise ValueError('no client valid!')
                return self.__client_ref
